Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/pages/news/[id].tsx
6095 views
1
/*
2
* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Alert, Breadcrumb, Col, Layout, Radio, Row } from "antd";
7
import { GetServerSidePropsContext } from "next";
8
import { useRouter } from "next/router";
9
import NextHead from "next/head";
10
import dayjs from "dayjs";
11
12
import { getNewsItemUserPrevNext } from "@cocalc/database/postgres/news";
13
import getCustomize from "@cocalc/database/settings/customize";
14
import { Icon } from "@cocalc/frontend/components/icon";
15
import { markdown_to_cheerio } from "@cocalc/frontend/markdown";
16
import { slugURL } from "@cocalc/util/news";
17
import { NewsPrevNext } from "@cocalc/util/types/news";
18
19
import Footer from "components/landing/footer";
20
import Head from "components/landing/head";
21
import Header from "components/landing/header";
22
import A from "components/misc/A";
23
import { News } from "components/news/news";
24
import { NewsWithStatus } from "components/news/types";
25
import { useDateStr } from "components/news/useDateStr";
26
import Loading from "components/share/loading";
27
import { MAX_WIDTH, NOT_FOUND } from "lib/config";
28
import { Customize, CustomizeType } from "lib/customize";
29
import useProfile from "lib/hooks/profile";
30
import { extractID } from "lib/news";
31
import withCustomize from "lib/with-customize";
32
33
interface Props {
34
customize: CustomizeType;
35
news: NewsWithStatus;
36
prev?: NewsPrevNext;
37
next?: NewsPrevNext;
38
metadata: {
39
title: string;
40
author: string;
41
url: string;
42
image: string;
43
published: string;
44
modified: string;
45
};
46
}
47
48
const formatNewsTime = (newsDate: NewsWithStatus["date"]) =>
49
(typeof newsDate === "number"
50
? dayjs.unix(newsDate)
51
: dayjs(newsDate)
52
).toISOString();
53
54
export default function NewsPage(props: Props) {
55
const { customize, news, prev, next, metadata } = props;
56
const { siteName } = customize;
57
const router = useRouter();
58
const profile = useProfile({ noCache: true });
59
const isAdmin = profile?.is_admin;
60
const dateStr = useDateStr(news);
61
const permalink = slugURL(news);
62
63
const title = `${news.title} – News – ${siteName}`;
64
65
function future() {
66
if (news.future && !isAdmin) {
67
return (
68
<Alert type="info" banner={true} message="News not yet published" />
69
);
70
}
71
}
72
73
function content() {
74
if (profile == null) return <Loading />;
75
if (!isAdmin && news.hide) {
76
return <Alert type="error" message="Not authorized" />;
77
}
78
if (isAdmin || !news.future) {
79
return <News news={news} showEdit={isAdmin} standalone />;
80
}
81
}
82
83
function breadcrumb() {
84
const items = [
85
{ key: "/", title: <A href="/">{siteName}</A> },
86
{ key: "/news", title: <A href="/news">News</A> },
87
{
88
key: "permalink",
89
title: (
90
<A href={permalink}>
91
{isAdmin || (!news.future && !news.hide) ? (
92
<>
93
{dateStr}: {news.title}
94
</>
95
) : (
96
"Not Authorized"
97
)}
98
</A>
99
),
100
},
101
];
102
return <Breadcrumb items={items} />;
103
}
104
105
function olderNewer() {
106
return (
107
<Radio.Group buttonStyle="outline" size="small">
108
<Radio.Button
109
disabled={!prev}
110
style={{ userSelect: "none" }}
111
onClick={() => {
112
prev && router.push(slugURL(prev));
113
}}
114
>
115
<Icon name="arrow-left" /> Older
116
</Radio.Button>
117
<Radio.Button
118
style={{ userSelect: "none" }}
119
onClick={() => {
120
router.push("/news");
121
}}
122
>
123
<Icon name="arrow-up" /> Overview
124
</Radio.Button>
125
<Radio.Button
126
disabled={!next}
127
style={{ userSelect: "none" }}
128
onClick={() => {
129
next && router.push(slugURL(next));
130
}}
131
>
132
<Icon name="arrow-right" /> Newer
133
</Radio.Button>
134
</Radio.Group>
135
);
136
}
137
138
function renderTop() {
139
return (
140
<Row justify="space-between" gutter={15} style={{ margin: "30px 0" }}>
141
<Col>{breadcrumb()}</Col>
142
<Col>{olderNewer()}</Col>
143
</Row>
144
);
145
}
146
147
return (
148
<Customize value={customize}>
149
<Head title={title} />
150
<NextHead>
151
<meta property="og:type" content="article" />
152
153
<meta property="og:title" content={metadata.title} />
154
<meta property="og:url" content={metadata.url} />
155
<meta property="og:image" content={metadata.image} />
156
157
<meta property="article:published_time" content={metadata.published} />
158
<meta property="article:modified_time" content={metadata.modified} />
159
</NextHead>
160
<Layout>
161
<Header />
162
<Layout.Content
163
style={{
164
backgroundColor: "white",
165
}}
166
>
167
<div
168
style={{
169
minHeight: "75vh",
170
maxWidth: MAX_WIDTH,
171
padding: "30px 15px",
172
margin: "0 auto",
173
}}
174
>
175
{renderTop()}
176
{future()}
177
{content()}
178
</div>
179
<Footer />
180
</Layout.Content>
181
</Layout>
182
</Customize>
183
);
184
}
185
186
export async function getServerSideProps(context: GetServerSidePropsContext) {
187
const { query } = context;
188
const id = extractID(query.id);
189
if (id == null) return NOT_FOUND;
190
191
try {
192
const { news, prev, next } = await getNewsItemUserPrevNext(id);
193
const { siteName, siteURL } = await getCustomize();
194
195
if (news == null) {
196
throw new Error(`not found`);
197
}
198
199
// Extract image URL from parsed Markdown. By converting to HTML first, we
200
// automatically add support for HTML that's been embedded into Markdown.
201
//
202
const $markdown = markdown_to_cheerio(news.text);
203
const imgSrc = $markdown("img").first().attr("src");
204
205
// Format published time
206
//
207
const publishedTime = formatNewsTime(news.date);
208
209
// Get the last-modified time by sorting the post history by timestamp,
210
// reversing it, and parsing the first element in that array.
211
//
212
const newsModificationTimestamps = Object.keys(news.history || {})
213
.map(Number)
214
.filter((ts) => !Number.isNaN(ts))
215
.sort()
216
.reverse();
217
218
const modifiedTime = newsModificationTimestamps.length
219
? formatNewsTime(newsModificationTimestamps[0])
220
: publishedTime;
221
222
const metadata: Props["metadata"] = {
223
title: news.title,
224
url: `${siteURL}${slugURL(news)}`,
225
image: imgSrc || "",
226
published: publishedTime,
227
modified: modifiedTime,
228
author: `${siteName}`,
229
};
230
231
return await withCustomize({
232
context,
233
props: {
234
news,
235
prev,
236
next,
237
metadata,
238
},
239
});
240
} catch (err) {
241
console.warn(`Error getting news with id=${id}`, err);
242
}
243
244
return NOT_FOUND;
245
}
246
247